/*******************************************************************************
 * Copyright 2012 Thomas Letsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *******************************************************************************/
package com.googlecode.javaeeutils.jpa;

import static javax.xml.bind.annotation.XmlAccessType.FIELD;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlAccessorType;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import com.googlecode.javaeeutils.jpa.manager.EntityManagerHelper;
import com.googlecode.javaeeutils.reflection.ReflectionUtils;

/**
 * Base class for all persistent entities. It defines some common best practices:
 * <ul>
 * <li>Every entity has a long id (technical primary key)</li>
 * <li>Every entity has a long version for optimistic locking</li>
 * <li>Every entity has a field createdAt and createdBy</li>
 * <li>JaXB is configured to access the fields and not the properties, since this is what hibernate also does.</li>
 * </ul>
 * Additionally the toString method is implemented in a way that it prints out all non-lazy properties.
 * 
 * @author thomas.letsch.de@gmail.com
 * 
 */
@MappedSuperclass
@XmlAccessorType(FIELD)
public class PersistentEntity {

    public static final String DEFAULT_USER = "system";

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private Long version;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(updatable = false)
    protected Date createdAt;

    @Size(max = 100)
    @Column(updatable = false)
    protected String createdBy;

    @PrePersist
    public void initializeCreationInfo() {
        Date now = new Date();
        createdAt = now;
        createdBy = DEFAULT_USER;
    }

    @SuppressWarnings("unchecked")
    public <ENTITY extends PersistentEntity> ENTITY getSavedEntity(ENTITY entity) {
        if (entity == null) {
            return null;
        }
        if (entity.getId() == null) {
            return entity;
        }
        return (ENTITY) EntityManagerHelper.get().find(entity.getClass(), entity.getId());
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public synchronized <ENTITY extends EntityWithNaturalKey> List<ENTITY> mergeLists(List<ENTITY> existing, List<ENTITY> newItems) {
        if (newItems == null) {
            return existing;
        }
        if (existing == null) {
            existing = new ArrayList<ENTITY>();
        }
        if (existing == newItems) {
            return existing;
        }
        for (ENTITY entity : newItems) {
            ENTITY savedEntity = (ENTITY) entity.findByNaturalKey();
            if (existing.contains(savedEntity)) {
                existing.get(existing.indexOf(savedEntity)).copyFrom(entity);
            } else {
                if (savedEntity != null) {
                    savedEntity.copyFrom(entity);
                    existing.add(savedEntity);
                } else {
                    entity.linkMasterData();
                    existing.add(entity);
                }
            }
        }
        return existing;
    }

    public void loadNeededRelations() {
    }

    public void linkMasterData() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String toString() {
        ReflectionToStringBuilder builder = new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
        builder.setExcludeFieldNames(getExcludeFields());
        return builder.toString();
    }

    @Override
    public int hashCode() {
        if (id != null) {
            return id.intValue();
        }
        return HashCodeBuilder.reflectionHashCode(this, getExcludeFields());
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass().equals(obj.getClass()) && id != null) {
            PersistentEntity other = (PersistentEntity) obj;
            return id.equals(other.id);
        }
        return EqualsBuilder.reflectionEquals(this, obj, getExcludeFields());
    }

    protected String[] getExcludeFields() {
        List<String> excludeFields = new ArrayList<String>();
        for (Field field : ReflectionUtils.getAllInstanceFields(getClass())) {
            if (Calendar.class.isAssignableFrom(field.getClass())) {
                excludeFields.add(field.getName());
            }
            if (isLazy(field)) {
                excludeFields.add(field.getName());
            }
        }
        return excludeFields.toArray(new String[] {});
    }

    protected boolean isLazy(Field field) {
        boolean isLazy = false;
        isLazy = isLazy || isLazy(field.getAnnotation(OneToOne.class));
        isLazy = isLazy || isLazy(field.getAnnotation(OneToMany.class));
        isLazy = isLazy || isLazy(field.getAnnotation(ManyToOne.class));
        isLazy = isLazy || isLazy(field.getAnnotation(ManyToMany.class));
        return isLazy;
    }

    private boolean isLazy(ManyToMany annotation) {
        if (annotation != null) {
            FetchType fetchType = annotation.fetch();
            return FetchType.LAZY.equals(fetchType);
        }
        return false;
    }

    private boolean isLazy(ManyToOne annotation) {
        if (annotation != null) {
            FetchType fetchType = annotation.fetch();
            return FetchType.LAZY.equals(fetchType);
        }
        return false;
    }

    private boolean isLazy(OneToMany annotation) {
        if (annotation != null) {
            FetchType fetchType = annotation.fetch();
            return FetchType.LAZY.equals(fetchType);
        }
        return false;
    }

    protected boolean isLazy(OneToOne annotation) {
        if (annotation != null) {
            FetchType fetchType = annotation.fetch();
            return FetchType.LAZY.equals(fetchType);
        }
        return false;
    }

    public Long getVersion() {
        return version;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public void setVersion(Long version) {
        this.version = version;
    }

}
